package com.songyang.blog.redisLock;

import com.songyang.blog.util.IpUtils;
import com.songyang.blog.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class redisLock implements Runnable {
    RedisTemplate redisTemplate;
    RedisUtil redisUtil;
    String threadName = "";
    String redisListKey = "viewList";//消息队列的key
    String keyTotal = "totalView";//redis总量访问key
    String keyNow = "totalViewNow";//Redis今天访问key
    Integer totalView;//总访问量
    Integer totalViewNow;//获取今日访问量和总体访问量
    String redisLock = "songYangLock";//Redis分布式锁的key
    long redisLockTime = 10;//Redis锁过期时间
    Integer maxListSize = 1000;//队列支持最大的消息数量，超过就会抛异常
    Integer listTopForNotifySize = 15;//没有拿到锁的线程进入队列进行自旋锁的阈值
    Integer listTopForNotifyTimes = 150;//进行自旋锁转阻塞的次数
    CountDownLatch count = null;
    Integer time = 0;
    boolean timeFlag = false;

    /*
          在多线程的情况下，spring是不能通过@Autowired去自动注入的
       因为在默认情况下是为了线程安全不注入。
          可以通过applicationContext去获取，或者把这个放在构造方法里面
       这里使用的是构造方法得到这两个类。
    */
    public redisLock(RedisTemplate redisTemplate, RedisUtil redisUtil) {
        this.redisTemplate = redisTemplate;
        this.redisUtil = redisUtil;
        count = new CountDownLatch(99);
    }

    public static int getSecondTimestampTwo() {
        Date date = new Date();
        if (null == date) {
            return 0;
        }
        String timestamp = String.valueOf(date.getTime() / 1000);
        return Integer.valueOf(timestamp);
    }

    @Override
    public void run() {
        if (time == 0) time = getSecondTimestampTwo();
        //采用自旋锁的方式
        //incrTotalViewWithRedisLockBySpin(redisTemplate, redisUtil);
        //采用阻塞唤醒的方式，如果采用这种方式，请把主函数的run方法打开，该方法为轮询检查队列的状态，进行唤醒
        //incrTotalViewWithRedisLockByWaitAndNotify(redisTemplate, redisUtil);
        //采用先自旋在阻塞的方法
        incrTotalViewWithRedisLockBySpanAndNotify(redisTemplate, redisUtil);
//        try {
//            count.await();
//            if (!timeFlag) {
//                timeFlag = true;
//                System.out.println("100个线程共花费" + (getSecondTimestampTwo() - time) + "s,平均一个线程花费" + (getSecondTimestampTwo() - time) / 100 + " s");
//            }
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }

    }

    private void incrTotalView(RedisUtil redisUtil) {
//        count.countDown();
        totalView = (Integer) redisUtil.get(keyTotal);//
        totalViewNow = (Integer) redisUtil.get(keyNow);//获取今日访问量和总体访问量
        redisUtil.set(keyTotal, totalView + 1);
        redisUtil.set(keyNow, totalViewNow + 1);//自加一次写入Redis
    }

    public void incrTotalViewWithRedisLockBySpin(RedisTemplate redisTemplate, RedisUtil redisUtil) {
        String threadName = "";
        String redisListName = "";
        String redisLockValue = UUID.randomUUID().toString();
        boolean flag = true;//标志位，如果没有成功的自加，就会一直循环
        try {
            threadName = "Ip=" + IpUtils.getInnetIp() + "-ThreadId=" + Thread.currentThread().getId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Thread.currentThread().setName(threadName);
        //线程尝试拿到锁
        Boolean result = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
        if (!result) {
            //如果没有成功地拿到锁，就进入Redis的消息队列，等到唤醒
            List<String> keyList = redisTemplate.opsForList().range(redisListKey, 0, -1);
            if (keyList.size() > maxListSize)
                throw new RuntimeException("阻塞队列已经满了，不允许再插入新的线程");
            //入队操作，从栈的左侧入队
            redisTemplate.opsForList().leftPush(redisListKey, threadName);
            while (flag) {
                //获取队列的最后一个元素，判断最后一个元素是不是自己的线程名称
                List range = redisTemplate.opsForList().range(redisListKey, -1, -1);
                if (range == null) {
                    log.info("队列为空，休眠一秒钟");
                    sleepWithMILLISECONDS(10);
                }
                redisListName = (String) range.get(0);
                //if判断成功，说明队列当中的线程是自己，自己可以拿到锁取执行了
                if (redisListName != null && redisListName.equals(threadName)) {
                    while (true) {
                        //尝试取拿到锁
                        Boolean tryLock = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
                        if (tryLock) {
                            redisTemplate.opsForList().rightPop(redisListKey);//自己已经拿到锁了，从队列出队，跳出自旋锁，执行下面的内容
                            flag = false;//成功拿到锁，不需要循环
                            break;
                        } else {
                            log.info("到了" + threadName + "这个线程，但是没有拿到锁，睡眠一段时间在重新试");
                            sleepWithMILLISECONDS(100);
                        }
                    }
                } else {
                    //队列当中的不是自己休眠一会再去抢占
                    sleepWithMILLISECONDS(100);
                }
            }
        }
//        log.info(threadName + "成功拿到锁执行增长访问量！");
        incrTotalView(redisUtil);//拿到锁以后在进行自增访问量
        releaseRedisLock(redisLock, redisLockValue);
    }

    private void sleepWithMILLISECONDS(int milliseconds) {
        try {
            TimeUnit.MILLISECONDS.sleep(milliseconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void releaseRedisLock(String redisLock, String redisValue) {
        String getValue = (String) redisUtil.get(redisLock);
        if (getValue != null && getValue.equals(redisValue))
            redisTemplate.delete(redisLock);//处理成功释放锁
        log.info("逻辑处理成功成功！释放锁");
    }

    private void releaseRedisLockForWaitAndNotify(String redisLock, String redisValue) {
        String getValue = (String) redisUtil.get(redisLock);
        if (getValue != null && getValue.equals(redisValue))
            redisTemplate.delete(redisLock);//处理成功释放锁
        log.info("增长线程：逻辑处理成功成功！释放锁");
        Integer notifyThreadId = (Integer) redisTemplate.opsForValue().get("notifyThreadId");
        Thread notifyThread = findThread(notifyThreadId);

        if (notifyThread == null) {
            log.info("增长线程：唤醒 '唤醒线程'失败！");
            return;
        }
        Thread.State state = notifyThread.getState();
        if (state.toString().equals("WAITING")) {
            synchronized (notifyThread) {
                notifyThread.notify();
            }
            log.info("增长线程：唤醒 '唤醒线程'成功！");
        }
    }

    private static Thread findThread(long threadId) {
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        while (group != null) {
            Thread[] threads = new Thread[(int) (group.activeCount() * 1.2)];
            int count = group.enumerate(threads, true);
            for (int i = 0; i < count; i++) {
                if (threadId == threads[i].getId()) {
                    return threads[i];
                }
            }
            group = group.getParent();
        }
        return null;
    }

    private void incrTotalViewWithRedisLockByWaitAndNotify(RedisTemplate redisTemplate, RedisUtil redisUtil) {
        String redisLockValue = UUID.randomUUID().toString();
        try {
            threadName = IpUtils.getInnetIp() + "-" + Thread.currentThread().getId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Thread.currentThread().setName(threadName);
        while (true) {
            //线程尝试拿到锁
            Boolean result = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
            if (!result) {
                //如果没有成功的拿到锁，就进入Redis的消息队列，等到唤醒
                //入队操作，从栈的左侧入队
                // log.info("线程"+Thread.currentThread().getId()+"没有拿到锁，入列");
                redisTemplate.opsForList().leftPush(redisListKey, threadName);
                // log.info("当前队列还有"+redisTemplate.opsForList().range(redisListKey,0,-1).size()+"个元素等待");
                //入队之后进入阻塞，等待其他线程唤醒。
                synchronized (Thread.currentThread()) {
                    try {
                        Thread.currentThread().wait();//进入阻塞状态
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else//成功拿到锁，跳出循环，开始执行逻辑
                break;
        }
        log.info("增长线程：" + threadName + "成功拿到锁执行增长访问量！");
        incrTotalView(redisUtil);//拿到锁以后在进行自增访问量
        releaseRedisLockForWaitAndNotify(redisLock, redisLockValue);
        //  log.info("一共执行："+(System.currentTimeMillis()-start)+"ms时间");
    }

    private void incrTotalViewWithRedisLockBySpanAndNotify(RedisTemplate redisTemplate, RedisUtil redisUtil) {
        //思路：当一个线程没有拿到锁的时候，先自旋一段时间，或者队列内容不大的时候进行自旋
        //如果自旋的时间超过阈值或者队列的内容已经很多了，就直接阻塞
        Integer times = 0;
        Boolean flag = true;
        String redisLockValue = UUID.randomUUID().toString();
        try {
            threadName = IpUtils.getInnetIp() + "-" + Thread.currentThread().getId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Thread.currentThread().setName(threadName);
        while (flag) {
            //线程尝试拿到锁
            Boolean result = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
            if (!result) {//没有拿到锁
                //先进行自旋一会
                //当前队列的长度不是特别大
                if (redisTemplate.opsForList().range(redisListKey, 0, -1).size() < listTopForNotifySize) {
                    while (true) {
                        Boolean spanResult = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
                        if (!spanResult) {//自旋的时候没有拿到锁
                            sleepWithMILLISECONDS(110);
                            times++;
                        } else {//成功拿到锁
                            flag = false;
                            incrTotalView(redisUtil);//拿到锁以后在进行自增访问量
                            releaseRedisLockForWaitAndSpan(redisLock, redisLockValue);//释放锁
                            break;
                        }
                        if (times > listTopForNotifyTimes) {//说明自旋的次数到了，要入队阻塞
                            log.info(threadName + "自旋失败，进行阻塞");
                            times = 0;
                            waitAndPush(threadName);//阻塞入队
                            break;//当他被唤醒的时候，要跳出这个循环
                        }
                    }
                } else //如果队列很长了，就直接阻塞自己
                    waitAndPush(threadName);
            } else//成功拿到锁，跳出循环，开始执行逻辑
                break;
        }
        log.info("增长线程：" + threadName + "成功拿到锁执行增长访问量！");
        incrTotalView(redisUtil);//拿到锁以后在进行自增访问量
        releaseRedisLock(redisLock, redisLockValue);
//        releaseRedisLockForWaitAndSpan(redisLock, redisLockValue);
    }

    private void releaseRedisLockForWaitAndSpan(String redisLock, String redisValue) {
        String getValue = (String) redisUtil.get(redisLock);
        if (getValue != null && getValue.equals(redisValue))
            redisTemplate.delete(redisLock);//处理成功释放锁
        log.info("增长线程：逻辑处理成功成功！释放锁");
        Integer notifyThreadId = (Integer) redisTemplate.opsForValue().get("notifyThreadId");
        Thread notifyThread = findThread(notifyThreadId);

        if (notifyThread == null) {
            log.info("增长线程：唤醒 '唤醒线程'失败！");
            return;
        }
        Thread.State state = notifyThread.getState();
        if (state.toString().equals("WAITING")) {
            synchronized (notifyThread) {
                notifyThread.notify();
            }
            log.info("增长线程：唤醒 '唤醒线程'成功！");
        }
    }

    //入队并且阻塞
    private void waitAndPush(String threadName) {
        synchronized (Thread.currentThread()) {
            try {
                redisTemplate.opsForList().leftPush(redisListKey, threadName);//入队
                Thread.currentThread().wait();//进入阻塞状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
